package org.atlantis.grosbeak.pot;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.atlantis.grosbeak.lang.Exceptions;
import org.atlantis.grosbeak.logger.Logger;
import org.atlantis.grosbeak.logger.LoggerFactory;
import org.atlantis.grosbeak.pot.dialect.Dialect;
import org.atlantis.grosbeak.pot.elm.Query;
import org.atlantis.grosbeak.pot.elm.Update;
import org.atlantis.grosbeak.pot.transaction.TransactionProvider;

/**
 * Pot呈现给用户的接口类。用户进行持久化等操作都是通过此类来完成的。
 * 
 * @author <a href="mailto:seenoevil.cn@gmail.com">SeeNoEvil</a>
 * 
 */
public class Poter {

	private final Integer ZERO = new Integer(0);
	private final int[] EMPTY = new int[0];
	private final Logger logger = LoggerFactory.getLogger(Poter.class);
	private Dialect dialect;

	/**
	 * 通过数据库方言实例化一个Poter。
	 * 
	 * @param dialect
	 *            数据库方言（目前仅支持MyySQL和HSQLDB两种数据库，但是完全可以自己扩展）。
	 */
	public Poter(Dialect dialect) {
		this.dialect = dialect;
		logger.infoFmt("Set database dialect to: '%s'", dialect.getClass()
				.getName());
	}

	class InvocationHandlerImpl implements InvocationHandler {

		private ThreadLocal<BatchMode> batchTL = new ThreadLocal<BatchMode>();
		private Map<Method, SQLHandler> map;
		private TransactionProvider txProvider;

		public InvocationHandlerImpl(Map<Method, SQLHandler> map,
				TransactionProvider txProvider) {
			this.map = map;
			this.txProvider = txProvider;
		}

		public Object invoke(Object proxy, Method method, Object[] args)
				throws Throwable {
			String methodName = method.getName();
			boolean paramsEmpty = (method.getParameterTypes().length == 0);
			BatchMode bm = batchTL.get();
			if (paramsEmpty) {
				if ("prepareBatch".equals(methodName)) {
					if (bm != null)
						throw Exceptions
								.makeThrow(PotException.class,
										"Can not start a batch when the batch has not be executed.");
					batchTL.set(new BatchMode());
					return null;
				} else if ("executeBatch".equals(methodName)) {
					if (bm == null)
						throw Exceptions
								.makeThrow(PotException.class,
										"The batch is not exist, so can not execute it.");
					if (bm.isBatchExecuted())
						throw Exceptions.makeThrow(PotException.class,
								"The batch has be executed.");
					PreparedStatement pstmt = bm.getBatchPreparedStatement();
					try {
						if (pstmt != null)
							return pstmt.executeBatch();
						else
							return EMPTY;
					} catch (SQLException e) {
						throw Exceptions.wrapThrow(PotException.class, e);
					} finally {
						bm.setBatchExecuted();
					}
				} else if ("closeBatch".equals(methodName)) {
					if (bm == null)
						throw Exceptions.makeThrow(PotException.class,
								"The batch is not exist, so can not close it.");
					batchTL.remove();
					bm.destroy();
					return null;
				}
			}
			SQLHandler sqlHandler = map.get(method);
			if (sqlHandler != null) {
				if (bm != null) {
					if (sqlHandler instanceof UpdateSQLHandler) {
						if (bm.isBatchExecuted())
							throw Exceptions
									.makeThrow(PotException.class,
											"The batch is not exist, so can not close it.");
						UpdateSQLHandler uHandler = (UpdateSQLHandler) sqlHandler;
						if (bm.getBatchUpdateSQLHandler() == null) {
							bm.setBatchUpdateSQLHandler(uHandler);
						} else if (bm.getBatchUpdateSQLHandler() != uHandler) {
							throw Exceptions
									.makeThrow(
											PotException.class,
											"Can not execute batch update with method '%s'",
											method.toString());
						}
						PreparedStatement pstmt = uHandler.prepareBatch(
								txProvider, bm.getBatchPreparedStatement(),
								args);
						bm.setBatchPreparedStatement(pstmt);
						return uHandler.getReturnAsInt() ? ZERO : null;
					}
				}
				return sqlHandler.handle(txProvider, args);
			}
			return method.invoke(proxy, args);
		}
	}

	class BatchMode {
		private PreparedStatement pstmt = null;
		private UpdateSQLHandler handler = null;
		private boolean executed = false;

		public PreparedStatement getBatchPreparedStatement() {
			return pstmt;
		}

		public void setBatchPreparedStatement(PreparedStatement pstmt) {
			this.pstmt = pstmt;
		}

		public boolean isBatchExecuted() {
			return executed;
		}

		public void setBatchExecuted() {
			this.executed = true;
		}

		public UpdateSQLHandler getBatchUpdateSQLHandler() {
			return handler;
		}

		public void setBatchUpdateSQLHandler(UpdateSQLHandler handler) {
			this.handler = handler;
		}

		public void destroy() {
			if (pstmt != null) {
				PotKit.safelyClose(pstmt);
				pstmt = null;
			}
		}
	}

	@SuppressWarnings("unchecked")
	public <T> T createPot(Class<T> clazz, TransactionProvider txProvider)
			throws PotException {
		if (!clazz.isInterface()) {
			throw Exceptions.makeThrow(PotException.class,
					"'%s' must be a interface.", clazz.getName());
		}
		Map<Method, SQLHandler> map = new HashMap<Method, SQLHandler>();
		for (Method method : clazz.getMethods()) {
			if (method.getParameterTypes().length == 0) {
				String name = method.getName();
				if ("prepareBath".equals(name) || "executeBatch".equals(name)
						|| "closeBatch".equals(name)) {
					continue;
				}
			}
			map.put(method, createSQLHandler(method));
		}
		T t = (T) Proxy.newProxyInstance(clazz.getClassLoader(),
				new Class<?>[] { clazz }, new InvocationHandlerImpl(map,
						txProvider));
		return t;
	}

	private SQLHandler createSQLHandler(Method method) throws PotException {
		Query query = method.getAnnotation(Query.class);
		Update update = method.getAnnotation(Update.class);
		if (query != null && update != null) {
			throw Exceptions
					.makeThrow(
							PotException.class,
							"The @Query and @Update can not exists in method '%s' at same time.",
							method.getName());
		}
		if (query == null && update == null)
			throw Exceptions.makeThrow(PotException.class,
					"@Query or @Update must be exists in method '%s'.", method
							.getName());
		return (query != null) ? new QuerySQLHandler(method, dialect)
				: new UpdateSQLHandler(method);
	}
}
